package com.jxmobi.mediator.rest;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jxmobi.mediator.exception.ErrorDetail;
import com.jxmobi.mediator.exception.IllegalOperationException;
import com.jxmobi.mediator.exception.RestCallFailedException;
import com.jxmobi.mediator.util.page.PageWrapper;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.*;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.utils.HttpClientUtils;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

import static com.jxmobi.mediator.rest.RestEndpointConfig.BasicAuthConfig;


/**
 *  base for the REST call
 *
 *  NOTE: you should call the initialize first to give an object of RestEndpointConfig
 *
 *  http://www.baeldung.com/httpclient-connection-management
 *  https://hc.apache.org/httpcomponents-client-ga/examples.html
 *
 * @author fei <a href="mailto:xchen@jxmobi.com">Email the author</a>
 * @version 1.0 6/7/16
 */
public class RestCallStack {

    private CloseableHttpClient instance;

    private ObjectMapper mapper;

    private RestEndpointConfig endpointConfig;

    public static RestCallStack create(RestEndpointConfig config) {
        return new RestCallStack(config);
    }

    private RestCallStack(){}

    private RestCallStack(RestEndpointConfig config) {
        initialize(config);
    }

    private void initialize(RestEndpointConfig config) {
        mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        instance = HttpClients.createDefault();;
        this.endpointConfig = config;
    }

    /**
     *  retrieve one single pojo from remote endpoint
     * @param appUrl remote RESTful endpoint
     * @param clazz accept POJO's Class
     * @param <BO> POJO's type
     * @return an POJO object
     */
    public <BO,VO> BO retrieveSinglePojoFromRemote(String appUrl, Class<? extends BO> clazz, VO vo ) throws IOException, URISyntaxException, RestCallFailedException, IllegalAccessException {
        List<NameValuePair> pairs = getObjectNameValuePairs(vo);
        return retrieveSinglePojoFromRemote(appUrl, clazz, pairs);
    }

    /**
     *  retrieve one single pojo from remote endpoint
     * @param appUrl remote RESTful endpoint
     * @param clazz accept POJO's Class
     * @param <BO> POJO's type
     * @return an POJO object
     */
    public <BO> BO retrieveSinglePojoFromRemote(String appUrl, Class<? extends BO> clazz, List<NameValuePair> params ) throws URISyntaxException, RestCallFailedException {
        if (endpointConfig == null)
            throw new IllegalOperationException("you have to call initialize(RestEndpointConfig config) method first");
        URIBuilder builder = endpointConfig.uriBuilder(appUrl, params);

        URI uri = builder.build();

        HttpGet request = new HttpGet(uri);

        request.setHeaders(generalHeaders(endpointConfig.getAuthConfig()));

        CloseableHttpResponse response = null;
        try {

            response = instance.execute(request);

            HttpEntity entity = response.getEntity();

            if (entity == null)
                return null;

            String bodyAsString = EntityUtils.toString(entity);

            StatusLine statusLine = response.getStatusLine();
            if (statusLine.getStatusCode() >= 400) {
                ErrorDetail errorDetail = mapper.readValue(bodyAsString, ErrorDetail.class);
                throw new RestCallFailedException(String.format("PUT method failed for %s", errorDetail.getDetail()), statusLine.getStatusCode());
            }

            EntityUtils.consume(entity);

            return mapper.readValue(bodyAsString, clazz);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            HttpClientUtils.closeQuietly(response);
        }

        return null;
    }

    /**
     * retrieve a page of BO from remote client
     * @param appUrl remote RESTful endpoint
     * @param clazz accept POJO's Class
     * @param <BO> actual POJO
     * @return one page of POJO
     * @throws IOException
     */
    public <BO> PageWrapper<BO> retrievePojosFromRemoteByPage(String appUrl, Class<? extends BO> clazz) throws RestCallFailedException, URISyntaxException {
        if (endpointConfig == null)
            throw new IllegalOperationException("you have to call initialize(RestEndpointConfig config) method first");
        URIBuilder builder = endpointConfig.uriBuilder(appUrl);

        URI uri = builder.build();

        HttpGet request = new HttpGet(appUrl);

        request.setHeaders(generalHeaders(endpointConfig.getAuthConfig()));
        CloseableHttpResponse response = null;

        try {
            response = instance.execute(request);

            HttpEntity entity = response.getEntity();

            if (entity == null) {
                return null;
            }

            String bodyAsString = EntityUtils.toString(entity);

            StatusLine statusLine = response.getStatusLine();
            if (statusLine.getStatusCode() >= 400) {
                ErrorDetail errorDetail = mapper.readValue(bodyAsString, ErrorDetail.class);
                throw new RestCallFailedException(String.format("PUT method failed for %s", errorDetail.getDetail()), statusLine.getStatusCode());
            }

            ObjectMapper mapper = new ObjectMapper();

            JavaType type = mapper.getTypeFactory().constructParametrizedType(PageWrapper.class, PageWrapper.class, clazz);

            EntityUtils.consume(entity);

            return mapper.readValue(bodyAsString, type);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            HttpClientUtils.closeQuietly(response);
        }

        return null;

    }

    /**
     *  make a post call to remote endpoint.
     * @param appUrl url location of the endpoint
     * @param clazz response's class type
     * @param vo post parameter pojo object
     * @param <BO> response's class
     * @param <VO> parameter's class
     * @return
     * @throws URISyntaxException
     * @throws IOException
     */
    public <BO, VO> BO postSinglePojoToRemote(String appUrl, Class<? extends BO> clazz, VO vo) throws RestCallFailedException, URISyntaxException {
        if (endpointConfig == null)
            throw new IllegalOperationException("you have to call initialize(RestEndpointConfig config) method first");
        URIBuilder builder = endpointConfig.uriBuilder(appUrl);

        URI uri = builder.build();

        System.out.println(uri.toString());

        HttpPost request = new HttpPost(uri);

        request.setHeaders(generalHeaders(endpointConfig.getAuthConfig()));

        CloseableHttpResponse response = null;
        try {

            String jsonStr = mapper.writeValueAsString(vo);

            System.out.println(jsonStr);

            StringEntity jsonEntity = new StringEntity(jsonStr);

            request.setEntity(jsonEntity);

            response = instance.execute(request);

            HttpEntity entity = response.getEntity();

            if (entity ==null || entity.getContentLength() == 0)
                return null;


            String bodyAsString = EntityUtils.toString(entity);

            System.out.println(bodyAsString);

            StatusLine statusLine = response.getStatusLine();

            System.out.println(statusLine.getStatusCode());

            if (statusLine.getStatusCode() >= 400) {
                ErrorDetail errorDetail = mapper.readValue(bodyAsString, ErrorDetail.class);
                throw new RestCallFailedException(String.format("POST method failed for %s", errorDetail.getDetail()), statusLine.getStatusCode());
            }

            return mapper.readValue(bodyAsString, clazz);
        }  catch (IOException e) {
            e.printStackTrace();
        } finally {
            HttpClientUtils.closeQuietly(response);
        }

        return null;

    }


    /**
     *  make a put call to remote endpoint.
     * @param appUrl url location of the endpoint
     * @param clazz response's class type
     * @param vo post parameter pojo object
     * @param <BO> response's class
     * @param <VO> parameter's class
     * @return
     * @throws URISyntaxException
     * @throws IOException
     */
    public <BO, VO> BO putSinglePojoToRemote(String appUrl, Class<? extends BO> clazz, VO vo) throws URISyntaxException, RestCallFailedException {
        if (endpointConfig == null)
            throw new IllegalOperationException("you have to call initialize method first");
        URIBuilder builder = endpointConfig.uriBuilder(appUrl);

        URI uri = builder.build();

        HttpPut request = new HttpPut(uri);

        request.setHeaders(generalHeaders(endpointConfig.getAuthConfig()));
        CloseableHttpResponse response = null;

        try {

            StringEntity jsonParams = new StringEntity(mapper.writeValueAsString(vo));

            request.setEntity(jsonParams);

            response = instance.execute(request);

            HttpEntity entity = response.getEntity();

            String bodyAsString = EntityUtils.toString(entity);

            StatusLine statusLine = response.getStatusLine();
            if (statusLine.getStatusCode() >= 400) {
                ErrorDetail errorDetail = mapper.readValue(bodyAsString, ErrorDetail.class);
                throw new RestCallFailedException(String.format("PUT method failed for %s", errorDetail.getDetail()), statusLine.getStatusCode());
            }

            EntityUtils.consume(entity);

            return mapper.readValue(bodyAsString, clazz);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            HttpClientUtils.closeQuietly(response);
        }

        return null;

    }

    /**
     *  close the HttpClient
     */
    public void closeClient() {
        HttpClientUtils.closeQuietly(instance);
    }


    /**
     *  basic authentication header
     *
     * @param config auth config
     * @return
     */
    private Header[] generalHeaders(BasicAuthConfig config) {
        Header contentTypeHeader = new BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json;utf-8");    // 请求的类型是 json 类型

        String authorization = config.getUsername() + ":" + config.getPassword();
        String basic = new String(Base64.encodeBase64
                (authorization.getBytes(Charset.forName("US-ASCII"))));
        BasicHeader authHeader = new BasicHeader(HttpHeaders.AUTHORIZATION,  "Basic " + basic);

        System.err.println(" authentication header: " + authHeader.getName() + " ---- " + authHeader.getValue());

        return new Header[]{contentTypeHeader, authHeader};
    }


    public static List<NameValuePair> getObjectNameValuePairs(Object obj) throws IllegalArgumentException, IllegalAccessException {
        ArrayList<NameValuePair> list = new ArrayList<NameValuePair>();
        for (Field field : obj.getClass().getDeclaredFields()) {
            field.setAccessible(true); // if you want to modify private fields
            NameValuePair nameValuePair = new BasicNameValuePair(field.getName(), field.get(obj).toString());
            list.add(nameValuePair);
        }
        return list;
    }
}
